package cn.lg.soar.generator.util;

import cn.lg.soar.common.util.data.DataUtil;
import cn.lg.soar.common.util.data.StringUtil;
import cn.lg.soar.database.base.controller.BaseController;
import cn.lg.soar.database.base.entity.BaseEntity;
import cn.lg.soar.database.base.entity.NumberTreeEntity;
import cn.lg.soar.database.base.entity.PathTreeEntity;
import cn.lg.soar.database.base.service.IBaseService;
import cn.lg.soar.database.base.service.INumberTreeService;
import cn.lg.soar.database.base.service.IPathTreeService;
import cn.lg.soar.database.base.service.impl.BaseServiceImpl;
import cn.lg.soar.database.base.service.impl.NumberTreeServiceImpl;
import cn.lg.soar.database.base.service.impl.PathTreeServiceImpl;
import cn.lg.soar.generator.CodeGenerator;
import cn.lg.soar.generator.enums.TemplateEnum;
import cn.lg.soar.generator.model.*;
import com.baomidou.mybatisplus.core.exceptions.MybatisPlusException;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.extension.service.IService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import freemarker.template.Configuration;
import freemarker.template.TemplateException;
import net.sf.jsqlparser.JSQLParserException;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.schema.Table;
import net.sf.jsqlparser.statement.create.table.ColumnDefinition;
import net.sf.jsqlparser.statement.create.table.CreateTable;
import net.sf.jsqlparser.statement.create.table.Index;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.PrintWriter;
import java.time.LocalDate;
import java.util.*;
import java.util.stream.Collectors;

/**
 * @author luguoxiang
 * 开源项目：https://gitee.com/lgx1992/lg-soar 求star！请给我star！请帮我点个star！
 */
public class Utils {
    /**
     * 判断数组是否为空
     * @param array
     * @return
     */
    public static boolean isEmptyArray(Object[] array) {
        return null == array || array.length == 0;
    }

    /**
     * 读取控制台输入内容
     * @param tip 提示信息
     * @return
     */
    public static String scanner(String tip) {
        Scanner scanner = new Scanner(System.in);
        System.out.println("请输入" + tip + "：");
        if (scanner.hasNext()) {
            String ipt = scanner.next();
            if (!StringUtils.isBlank(ipt)) {
                return ipt;
            }
        }
        throw new MybatisPlusException("请输入正确的" + tip + "！");
    }

    /**
     * 解析表信息
     * @param ddl
     * @return
     */
    public static TableInfo parseTableInfo(String ddl) {
        CreateTable stmt;
        try {
            stmt = (CreateTable) CCJSqlParserUtil.parse(ddl);
        } catch (JSQLParserException e) {
            throw new RuntimeException(e);
        }
        List<Index> indexes = stmt.getIndexes();
        Optional<Index> optional = indexes.stream()
                .filter(x -> "PRIMARY KEY".equals(x.getType()))
                .findAny();
        String regex = "[`'\"]";
        List<String> pks = optional.map(Index::getColumnsNames)
                .orElse(Collections.emptyList())
                .stream().map(x -> x.replaceAll(regex, ""))
                .collect(Collectors.toList());

        TableInfo tableInfo = new TableInfo();
        Table table = stmt.getTable();
        tableInfo.setName(table.getName().replaceAll(regex, ""));
        List<?> tableOptionsStrings = stmt.getTableOptionsStrings();
        for (int i = 0, size = tableOptionsStrings.size(); i < size; i++) {
            Object o = tableOptionsStrings.get(i);
            if ("COMMENT".equals(o) && (i + 2) < size) {
                Object o1 = tableOptionsStrings.get(i + 2);
                if (o1 instanceof String) {
                    tableInfo.setComment(((String) o1).replaceAll(regex, ""));
                }
            }
        }

        List<ColumnDefinition> list = stmt.getColumnDefinitions();
        List<ColumnInfo> columnInfos = new ArrayList<>(list.size());
        for (ColumnDefinition d : list) {
            List<String> columnSpecs = d.getColumnSpecs();
            String comment = "";
            boolean notNull = false;
            for (int i = 0, size = columnSpecs.size(); i < size; i++) {
                String s = columnSpecs.get(i);
                if ("COMMENT".equals(s) && (i + 1) < size) {
                    comment = columnSpecs.get(i + 1).replaceAll(regex, "");
                }
                if ("NOT".equals(s) && (i + 1) < size) {
                    notNull = "NULL".equals(columnSpecs.get(i + 1));
                }
            }
            String name = d.getColumnName().replaceAll(regex, "");
            String field = StringUtil.underscoreToCamel(name);;
//                    show create table XXX
            String type = d.getColDataType().getDataType();
            boolean pk = pks.contains(name);
            List<String> args = d.getColDataType().getArgumentsStringList();
            ColumnInfo columnInfo = new ColumnInfo();
            columnInfo.setName(name);
            columnInfo.setPropertyName(field);
            columnInfo.setType(type);
            columnInfo.setComment(comment);
            columnInfo.setArgs(args);
            columnInfo.setNullable(!notNull);
            columnInfo.setPk(pk);
            columnInfos.add(columnInfo);
        }
        tableInfo.setColumnInfos(columnInfos);
        return tableInfo;
    }

    private final static String bs = "create_time,update_time,create_by,update_by";
    private final static String[] bColumns = bs.split(",");
    private final static String[] ntColumns = (bs + ",parent_id,left_no,right_no").split(",");
    private final static String[] ptColumns = (bs + ",parent_id,ancestor_link,sort").split(",");

    public static GenConfig genConfig(TableInfo tableInfo) {
        GenConfig genConfig = new GenConfig();
        String tableName = tableInfo.getName();
        String module = "";
        String modelName;
        String tablePrefix = "";
        int index = tableName.indexOf("_");
        if (index == -1) {
            modelName = StringUtil.underscoreToCamel(tableName);
        } else {
            tablePrefix = tableName.substring(0, index + 1);
            module = tablePrefix.substring(0, index).replaceAll("_", "");
            modelName = StringUtil.upperFirstCase(
                    StringUtil.underscoreToCamel(tableName.substring(index + 1))
            );
        }
        genConfig.setTableName(tableInfo.getName());
        genConfig.setTablePrefix(tablePrefix);
        genConfig.setComment(tableInfo.getComment());
        genConfig.setAuthor(System.getProperty("user.name"));
        genConfig.setOutputDir(System.getProperty("user.dir") + File.separator + "gen");
        genConfig.setApiPath(module +"/"+ StringUtil.lowerFirstCase(modelName));
        genConfig.setModelName(modelName);

        List<ColumnInfo> columnInfos = tableInfo.getColumnInfos();
        genConfig.setPkName(columnInfos.stream().filter(ColumnInfo::isPk).findFirst().map(ColumnInfo::getName).orElse(null));
        Set<String> baseFields;
        if (contains(columnInfos, ptColumns)) {
            baseFields = DataUtil.arrayToSet(ptColumns);
            genConfig.setTemplate(TemplateEnum.PATH_TREE);
        } else if (contains(columnInfos, ntColumns)) {
            baseFields = DataUtil.arrayToSet(ntColumns);
            genConfig.setTemplate(TemplateEnum.NUMBER_TREE);
        } else if (contains(columnInfos, bColumns)) {
            baseFields = DataUtil.arrayToSet(bColumns);
            genConfig.setTemplate(TemplateEnum.BASE);
        } else {
            baseFields = Collections.emptySet();
            genConfig.setTemplate(TemplateEnum.NONE);
        }

        List<JavaFieldConfig> javaFieldConfigs = new ArrayList<>(columnInfos.size());
        List<WebFieldConfig> webFieldConfigs = new ArrayList<>(columnInfos.size());
        for (ColumnInfo columnInfo : columnInfos) {
            String name = columnInfo.getName();
            String propertyName = columnInfo.getPropertyName();
            String comment = columnInfo.getComment();
            boolean pk = columnInfo.isPk();
            String type = columnInfo.getType();
            boolean nullable = columnInfo.isNullable();
            List<String> args = columnInfo.getArgs();

            WebFieldConfig webFieldConfig = new WebFieldConfig();
            webFieldConfig.setEnabled(!baseFields.contains(name) && !columnInfo.isPk());
            webFieldConfig.setName(name);
            webFieldConfig.setField(propertyName);
            webFieldConfig.setLabel(comment);
            webFieldConfig.setPk(pk);
            webFieldConfig.setDataType(toWebType(type, args));
            webFieldConfig.setNullable(nullable);
            webFieldConfigs.add(webFieldConfig);

            JavaFieldConfig javaFieldConfig = new JavaFieldConfig();
            javaFieldConfig.setEnabled(!baseFields.contains(name));
            javaFieldConfig.setName(name);
            javaFieldConfig.setPropertyName(propertyName);
            javaFieldConfig.setComment(comment);
            javaFieldConfig.setPk(pk);
            javaFieldConfig.setJavaType(toJavaType(type, args));
            javaFieldConfig.setNullable(nullable);
            if (!nullable) {
                String notNull;
                if (pk) {
                    notNull = "@NotNull(message = \"主键不能为空\", groups = { Update.class })";
                } else if ("String".equals(javaFieldConfig.getJavaType())) {
                    notNull = "@NotBlank(message = \"%s不能为空\", groups = { Create.class, Update.class })";
                } else {
                    notNull = "@NotNull(message = \"%s不能为空\", groups = { Create.class, Update.class })";
                }
                javaFieldConfig.addAnnotation(
                        String.format(notNull, comment)
                );
            }
            javaFieldConfigs.add(javaFieldConfig);
        }

        JavaConfig javaConfig = new JavaConfig();
        if (module.isEmpty()) {
            javaConfig.setParentPackages("cn.lg.soar");
        } else {
            javaConfig.setParentPackages("cn.lg.soar." + module);
        }
        javaConfig.setFields(javaFieldConfigs);
        genConfig.setJavaConfig(javaConfig);

        WebConfig webConfig = new WebConfig();
        webConfig.setDirectory(module +"/"+ modelName);
        webConfig.setFields(webFieldConfigs);
        genConfig.setWebConfig(webConfig);

        return genConfig;
    }

    public static void generate(GenConfig config) throws IOException, TemplateException {
        String tableName = config.getTableName();
        String modelName = config.getModelName();
        JavaConfig javaConfig = config.getJavaConfig();
        WebConfig webConfig = config.getWebConfig();

        Import imports = new Import();

        Map<String, Object> dataModel = new HashMap<>(64);
        dataModel.put("date", LocalDate.now().toString());
        dataModel.put("author", config.getAuthor());
        dataModel.put("comment", config.getComment());
        dataModel.put("tableName", tableName);
        dataModel.put("modelName", modelName);
        dataModel.put("apiPath", config.getApiPath());
        dataModel.put("pkName", config.getPkName());

        List<String> javaImports = new ArrayList<>(64);

        // 配置模板
        switch (config.getTemplate()) {
            case PATH_TREE:
                dataModel.put("templateName", "tree");
                imports.setBaseEntity(PathTreeEntity.class);
                imports.setBaseMapper(BaseMapper.class);
                imports.setBaseService(IPathTreeService.class);
                imports.setBaseServiceImpl(PathTreeServiceImpl.class);
                break;
            case NUMBER_TREE: // tree
                dataModel.put("templateName", "tree");
                imports.setBaseEntity(NumberTreeEntity.class);
                imports.setBaseMapper(BaseMapper.class);
                imports.setBaseService(INumberTreeService.class);
                imports.setBaseServiceImpl(NumberTreeServiceImpl.class);
                break;
            case BASE: // base
                dataModel.put("templateName", "base");
                imports.setBaseEntity(BaseEntity.class);
                imports.setBaseMapper(BaseMapper.class);
                imports.setBaseService(IBaseService.class);
                imports.setBaseServiceImpl(BaseServiceImpl.class);
                break;
            default: // none
                dataModel.put("templateName", "none");
//                packageInfo.setBaseEntity(BaseEntity.class);
                imports.setBaseMapper(BaseMapper.class);
                imports.setBaseService(IService.class);
                imports.setBaseServiceImpl(ServiceImpl.class);
                break;
        }
        imports.setBaseController(BaseController.class);


        //1.创建freeMarker配置实例
        Configuration cfg = new Configuration(Configuration.VERSION_2_3_29);
        //2.设置模板加载器：开始加载模板
        String template = CodeGenerator.class
                .getClassLoader()
                .getResource("templates")
                .getFile();
        cfg.setDirectoryForTemplateLoading(new File(template));

        // 输出目录
        String baseFile = config.getOutputDir() + "\\" + modelName;

        if (javaConfig != null) {
            JavaFieldConfig pkField = javaConfig.getFields().stream().filter(JavaFieldConfig::isPk).findFirst().get();
            String parentPackages = javaConfig.getParentPackages();
            ClassInfo entity = new ClassInfo(parentPackages + ".entity." + modelName);
            dataModel.put("entity", entity);
            ClassInfo mapper = new ClassInfo(parentPackages + ".mapper." + modelName + "Mapper");
            dataModel.put("mapper", mapper);
            ClassInfo service = new ClassInfo(parentPackages + ".service." + modelName + "Service");
            dataModel.put("service", service);
            ClassInfo serviceImpl = new ClassInfo(parentPackages + ".service.impl." + modelName + "ServiceImpl");
            dataModel.put("serviceImpl", serviceImpl);
            ClassInfo controller = new ClassInfo(parentPackages + ".controller." + modelName + "Controller");
            dataModel.put("controller", controller);
            dataModel.put("imports", imports);
            dataModel.put("jis", javaImports);
            dataModel.put("jc", javaConfig);
            dataModel.put("jl", javaConfig.getFields().stream().filter(JavaFieldConfig::isEnabled).collect(Collectors.toList()));
            dataModel.put("jpk", pkField);
            dataModel.put("pkType", pkField.getJavaType());

            //4. entity
            cfg.getTemplate("entity.java.ftl")
                    .process(dataModel, getPrintWriter(baseFile + "\\entity\\", entity+".java"));
            //4. mapper
            cfg.getTemplate("mapper.java.ftl")
                    .process(dataModel, getPrintWriter(baseFile + "\\mapper\\", mapper+".java"));
            //4. service
            cfg.getTemplate("service.java.ftl")
                    .process(dataModel, getPrintWriter(baseFile + "\\service\\", service+".java"));
            //4. serviceImpl
            cfg.getTemplate("serviceImpl.java.ftl")
                    .process(dataModel, getPrintWriter(baseFile + "\\service\\impl\\", serviceImpl+".java"));
            //4. controller
            cfg.getTemplate("controller.java.ftl")
                    .process(dataModel, getPrintWriter(baseFile + "\\controller\\", controller+".java"));
        }

        if(webConfig != null){
            WebFieldConfig pkField = webConfig.getFields().stream().filter(WebFieldConfig::isPk).findFirst().get();
            dataModel.put("wc", webConfig);
            dataModel.put("wl", webConfig.getFields());
            dataModel.put("wpk", pkField);

            String webFile = baseFile + "\\ui";
            //4. schema
            cfg.getTemplate("schema.ts.ftl")
                    .process(dataModel, getPrintWriter(webFile, "schema.ts"));
            //4. api
            cfg.getTemplate("api.ts.ftl")
                    .process(dataModel, getPrintWriter(webFile, "api.ts"));
            //4. List
            cfg.getTemplate("List.vue.ftl")
                    .process(dataModel, getPrintWriter(webFile, "List.vue"));
            //4. Edit
            cfg.getTemplate("Edit.vue.ftl")
                    .process(dataModel, getPrintWriter(webFile, "Edit.vue"));
            //4. Detail.vue
            cfg.getTemplate("Detail.vue.ftl")
                    .process(dataModel, getPrintWriter(webFile, "Detail.vue"));
        }
    }

    private static PrintWriter getPrintWriter(String path, String name) throws FileNotFoundException {
        File dir = new File(path);
        if (!dir.exists()) {
            dir.mkdirs();
        }
        return new PrintWriter(dir.getPath() + "\\" + name);
    }

    private static boolean contains(List<ColumnInfo> columnInfos, String...fields) {
        for (String field : fields) {
            boolean flag = true;
            for (ColumnInfo columnInfo : columnInfos) {
                if (field.equals(columnInfo.getName())) {
                    flag = false;
                    break;
                }
            }
            if (flag) {
                return false;
            }
        }
        return true;
    }

    private static String toJavaType(String type, List<String> args) {
        switch (type) {
            case "bigint": return "Long";
            case "bit": return "Boolean";
            case "tinyint":
                if (!DataUtil.isEmpty(args) && "1".equals(args.get(0))) {
                    return "Boolean";
                }
                return "Integer";
            case "datetime": return "LocalDateTime";
            case "date": return "LocalDate";
            default:
                if (type.contains("int")) {
                    return "Integer";
                }
                return "String";
        }
    }

    private static String toWebType(String type, List<String> args) {
        switch (type) {
            case "bit": return "boolean";
            case "tinyint":
                if (!DataUtil.isEmpty(args) && "1".equals(args.get(0))) {
                    return "boolean";
                }
                return "number";
            case "datetime": return "datetime";
            case "date": return "date";
            default:
                if (type.contains("int")) {
                    return "number";
                }
                return "string";
        }
    }

}
